Implementing Pull-to-Refresh and Infinite Scrolling Interfaces in React Native
Course: React Native Training
Topic: Implementing Pull-to-Refresh and Infinite Scrolling Interfaces
Course Link: React Native Training at JustAcademy
Demo Registration: Register for a Free Course Demo
Website: JustAcademy
1. Introduction to Pull-to-Refresh and Infinite Scrolling
Modern mobile applications frequently display dynamic data such as news articles, product listings, social media posts, messages, notifications, and transaction histories. As this data changes over time, users need convenient ways to refresh existing content and load additional records.
React Native provides components and event-handling mechanisms that make it possible to implement these experiences for Android and iOS applications.
1.1 What Is Pull-to-Refresh?
Pull-to-refresh is a mobile interaction in which a user pulls a scrollable list downward to request fresh data from a server or another data source.
For example, a user opens a news application, sees the latest articles, and pulls the list downward to retrieve newly published stories.
Common use cases include:
- Refreshing news feeds and social media timelines.
- Updating product listings in shopping applications.
- Refreshing notification and message lists.
- Updating dashboards and activity histories.
- Reloading weather information and live statistics.
1.2 What Is Infinite Scrolling?
Infinite scrolling is a technique in which an application loads additional records when the user approaches the end of a scrollable list. Instead of requiring users to select a traditional pagination button, the application automatically requests another page of data.
For example, an online shopping application may initially display 20 products. When the user scrolls near the bottom, the application requests the next 20 products and appends them to the existing list.
Common use cases include:
- Social media feeds.
- E-commerce product catalogs.
- News and blog applications.
- Chat history and activity timelines.
- Search results and media galleries.
1.3 Difference Between Pull-to-Refresh and Infinite Scrolling
| Feature |
Pull-to-Refresh |
Infinite Scrolling |
| Purpose |
Retrieve fresh or updated content. |
Retrieve additional content. |
| User action |
Pulls the list downward. |
Scrolls toward the end of the list. |
| Typical trigger |
Refresh gesture. |
End-reached event. |
| React Native support |
RefreshControl |
onEndReached |
| State updates |
Usually replaces the current dataset. |
Usually appends records to the dataset. |
| Loading indicator |
Refresh spinner. |
Footer activity indicator. |
2. Learning Objectives
After completing this topic, learners should be able to:
- Understand how pull-to-refresh and infinite scrolling work.
- Implement pull-to-refresh with React Native's
RefreshControl.
- Use
FlatList to display dynamic datasets efficiently.
- Implement pagination using
onEndReached.
- Manage loading, refreshing, error, and pagination states.
- Fetch data from REST APIs and process paginated responses.
- Prevent duplicate API calls and duplicate records.
- Handle empty states, network failures, and the end of a dataset.
- Improve performance and accessibility in large lists.
- Test the implementation on Android and iOS devices.
3. Understanding FlatList in React Native
FlatList is a React Native component designed to render scrollable lists efficiently. It renders items as needed rather than creating every list item at once, making it suitable for long or dynamic datasets.
3.1 Why Use FlatList?
- Supports efficient rendering of large lists.
- Provides built-in scrolling behavior.
- Supports pull-to-refresh through
refreshing and onRefresh.
- Supports infinite scrolling through
onEndReached.
- Provides
ListHeaderComponent and ListFooterComponent.
- Supports empty-list interfaces through
ListEmptyComponent.
- Supports stable item keys through
keyExtractor.
3.2 Basic FlatList Example
import React from 'react';
import { View, Text, FlatList } from 'react-native';
const users = [
{ id: '1', name: 'Aarav' },
{ id: '2', name: 'Priya' },
{ id: '3', name: 'Rahul' },
];
export default function UserList() {
return (
<View style={{ flex: 1, padding: 16 }}>
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text style={{ padding: 12 }}>
{item.name}
</Text>
)}
/>
</View>
);
}
In this example, data contains the list records, keyExtractor provides a unique key for every item, and renderItem defines how each record appears on the screen.
4. Implementing Pull-to-Refresh with RefreshControl
React Native provides the RefreshControl component for adding pull-to-refresh functionality to scrollable content. It is commonly attached to a FlatList through the refreshControl property.
4.1 Important Properties
| Property |
Description |
refreshing |
A Boolean indicating whether refresh is currently in progress. |
onRefresh |
Callback invoked when the user performs a refresh gesture. |
colors |
Android property for specifying refresh spinner colors. |
tintColor |
iOS property for the refresh indicator color. |
progressBackgroundColor |
Android property for the indicator background color. |
title |
Optional refresh text supported on iOS. |
4.2 Basic Pull-to-Refresh Implementation
import React, { useState, useCallback } from 'react';
import {
View,
Text,
FlatList,
RefreshControl,
} from 'react-native';
export default function RefreshExample() {
const [items, setItems] = useState([
{ id: '1', title: 'First Item' },
{ id: '2', title: 'Second Item' },
{ id: '3', title: 'Third Item' },
]);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
// Replace this example with an API request.
await new Promise(resolve => setTimeout(resolve, 1000));
setItems([
{ id: '4', title: 'Updated Item One' },
{ id: '5', title: 'Updated Item Two' },
{ id: '6', title: 'Updated Item Three' },
]);
} catch (error) {
console.error('Refresh failed:', error);
} finally {
setRefreshing(false);
}
}, []);
return (
<View style={{ flex: 1, padding: 16 }}>
<FlatList
data={items}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<Text style={{ padding: 16 }}>
{item.title}
</Text>
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#2563EB"
colors={['#2563EB']}
/>
}
/>
</View>
);
}
4.3 How the Implementation Works
- Initial rendering: The component displays the current items.
- User gesture: The user pulls the list downward.
- Refresh callback: React Native invokes
onRefresh.
- Loading state:
refreshing becomes true, showing the indicator.
- Data retrieval: The application requests updated information.
- State update: The refreshed records replace the previous records.
- Completion: The
finally block sets refreshing to false.
Important: The timeout in this demonstration simulates a network request. A real application should fetch data from its API and update state using the returned response.
5. Fetching Data from a REST API
Most production applications obtain list data from a backend service. The JavaScript fetch() API can send HTTP requests and retrieve JSON responses.
5.1 Example API Request
async function fetchPosts() {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts'
);
if (!response.ok) {
throw new Error('Unable to fetch posts');
}
const posts = await response.json();
return posts;
}
5.2 Understanding the Request
fetch() sends the request to the specified endpoint.
response.ok indicates whether the HTTP status is in the successful range.
response.json() parses the response body into JavaScript data.
throw new Error() allows the caller to handle unsuccessful responses.
- The returned data can be stored in component state.
The JSONPlaceholder endpoint above is a demonstration API. Production applications should use their own appropriate API endpoints and follow their pagination and authentication requirements.
6. Implementing Infinite Scrolling with FlatList
Infinite scrolling uses the onEndReached callback to request additional records when the list approaches its end.
6.1 Important Properties
| Property |
Purpose |
onEndReached |
Runs a callback when the list approaches the end. |
onEndReachedThreshold |
Controls how far from the end the callback can trigger, measured in visible-list lengths. |
ListFooterComponent |
Displays content at the end of the list, such as a loading spinner. |
data |
Contains the records to render. |
keyExtractor |
Provides stable, unique keys for list items. |
initialNumToRender |
Controls the initial number of items rendered. |
6.2 Pagination Concept
Suppose an API returns 10 records per page. The application can request page 1 during initial loading, page 2 when the user approaches the bottom, and page 3 when the user continues scrolling.
const PAGE_SIZE = 10;
async function fetchProducts(page) {
const response = await fetch(
`https://example.com/api/products?page=${page}&limit=${PAGE_SIZE}`
);
if (!response.ok) {
throw new Error('Unable to load products');
}
return response.json();
}
The example URL is illustrative. Replace it with an actual API endpoint that supports the specified pagination parameters.
7. Complete Example: Pull-to-Refresh and Infinite Scrolling
The following example combines both interactions in one screen. It uses a paginated demonstration API, replaces the list during refresh, appends records during pagination, displays loading indicators, and prevents overlapping requests within the component.
import React, {
useState,
useEffect,
useCallback,
useRef,
} from 'react';
import {
View,
Text,
FlatList,
RefreshControl,
ActivityIndicator,
StyleSheet,
} from 'react-native';
const API_URL = 'https://jsonplaceholder.typicode.com/posts';
const PAGE_SIZE = 10;
export default function PostsScreen() {
const [posts, setPosts] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
const [error, setError] = useState('');
const [hasMore, setHasMore] = useState(true);
const pageRef = useRef(1);
const requestLock = useRef(false);
const fetchPage = useCallback(async (page) => {
const response = await fetch(
`${API_URL}?_page=${page}&_limit=${PAGE_SIZE}`
);
if (!response.ok) {
throw new Error('Failed to load posts');
}
return response.json();
}, []);
const loadInitialPosts = useCallback(async () => {
if (requestLock.current) return;
requestLock.current = true;
setInitialLoading(true);
setError('');
try {
const data = await fetchPage(1);
setPosts(data);
pageRef.current = 1;
setHasMore(data.length === PAGE_SIZE);
} catch (err) {
setError(err.message || 'Something went wrong');
} finally {
setInitialLoading(false);
requestLock.current = false;
}
}, [fetchPage]);
useEffect(() => {
loadInitialPosts();
}, [loadInitialPosts]);
const onRefresh = useCallback(async () => {
if (requestLock.current) return;
requestLock.current = true;
setRefreshing(true);
setError('');
try {
const data = await fetchPage(1);
setPosts(data);
pageRef.current = 1;
setHasMore(data.length === PAGE_SIZE);
} catch (err) {
setError(err.message || 'Unable to refresh posts');
} finally {
setRefreshing(false);
requestLock.current = false;
}
}, [fetchPage]);
const loadMorePosts = useCallback(async () => {
if (
requestLock.current ||
loadingMore ||
refreshing ||
initialLoading ||
!hasMore
) {
return;
}
requestLock.current = true;
setLoadingMore(true);
setError('');
const nextPage = pageRef.current + 1;
try {
const data = await fetchPage(nextPage);
setPosts(previous => {
const existingIds = new Set(
previous.map(post => post.id)
);
const uniquePosts = data.filter(
post => !existingIds.has(post.id)
);
return [...previous, ...uniquePosts];
});
pageRef.current = nextPage;
setHasMore(data.length === PAGE_SIZE);
} catch (err) {
setError(err.message || 'Unable to load more posts');
} finally {
setLoadingMore(false);
requestLock.current = false;
}
}, [
fetchPage,
loadingMore,
refreshing,
initialLoading,
hasMore,
]);
const renderPost = ({ item }) => (
<View style={styles.card}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.body}>{item.body}</Text>
</View>
);
const renderFooter = () => {
if (loadingMore) {
return (
<View style={styles.footer}>
<ActivityIndicator size="small" color="#2563EB" />
<Text style={styles.footerText}>
Loading more posts...
</Text>
</View>
);
}
if (error && posts.length > 0) {
return (
<View style={styles.footer}>
<Text style={styles.error}>{error}</Text>
<Text>Scroll again to retry loading.</Text>
</View>
);
}
if (!hasMore && posts.length > 0) {
return (
<Text style={styles.endText}>
You have reached the end.
</Text>
);
}
return null;
};
if (initialLoading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#2563EB" />
<Text>Loading posts...</Text>
</View>
);
}
if (error && posts.length === 0) {
return (
<View style={styles.center}>
<Text style={styles.error}>{error}</Text>
<Text onPress={loadInitialPosts}>
Tap here to retry
</Text>
</View>
);
}
return (
<FlatList
style={styles.container}
contentContainerStyle={styles.listContent}
data={posts}
keyExtractor={item => String(item.id)}
renderItem={renderPost}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#2563EB"
colors={['#2563EB']}
/>
}
onEndReached={loadMorePosts}
onEndReachedThreshold={0.5}
ListFooterComponent={renderFooter}
ListEmptyComponent={
<Text style={styles.empty}>No posts available.</Text>
}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8FAFC',
},
listContent: {
padding: 16,
flexGrow: 1,
},
card: {
padding: 16,
marginBottom: 12,
backgroundColor: '#FFFFFF',
borderRadius: 10,
elevation: 2,
},
title: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
color: '#0F172A',
},
body: {
fontSize: 14,
lineHeight: 21,
color: '#475569',
},
footer: {
alignItems: 'center',
padding: 20,
gap: 8,
},
footerText: {
color: '#475569',
},
endText: {
textAlign: 'center',
padding: 20,
color: '#64748B',
},
empty: {
textAlign: 'center',
marginTop: 40,
color: '#64748B',
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
gap: 12,
},
error: {
color: '#DC2626',
textAlign: 'center',
marginBottom: 8,
},
});
7.1 Understanding the Complete Example
- Initial loading: The screen fetches the first page and displays a full-screen loading indicator.
- Pull-to-refresh: The user requests page 1 again, and the list is replaced with the latest records.
- Infinite scrolling: When the user reaches the configured threshold, the next page is requested.
- Appending records: New records are appended to the existing list instead of replacing it.
- Duplicate protection: A set of existing IDs filters duplicate records from the next response.
- Request locking: A ref prevents overlapping requests within this component.
- End detection: If fewer than 10 records are returned, the application assumes that there are no more pages.
- Error handling: The application displays an error message when a request fails.
- Footer feedback: The footer displays a loading spinner or a message when the end is reached.
Production considerations: This is an educational example. The request lock and state checks help prevent duplicate requests, but more complex applications may require a dedicated pagination state machine, request cancellation, retry controls, and protection against stale responses. Also, JSONPlaceholder returns a fixed demonstration dataset; the last-page detection is based on the requested page size.
8. Understanding Pagination Strategies
Pagination divides a large dataset into smaller responses. This reduces the amount of data transferred in a single request and helps applications remain responsive.
8.1 Page-Based Pagination
Page-based pagination uses a page number and a page size.
GET /api/products?page=1&limit=20
GET /api/products?page=2&limit=20
GET /api/products?page=3&limit=20
This strategy is straightforward when the backend supports numbered pages.
8.2 Offset-Based Pagination
Offset pagination uses a starting position and a limit.
GET /api/products?offset=0&limit=20
GET /api/products?offset=20&limit=20
GET /api/products?offset=40&limit=20
It works well for many applications, but changes in the underlying dataset can cause skipped or repeated records when items are inserted or removed between requests.
8.3 Cursor-Based Pagination
Cursor-based pagination uses a token or identifier representing a position in the dataset.
GET /api/products?limit=20
GET /api/products?limit=20&cursor=eyJpZCI6MjB9
The backend returns a continuation cursor with each response. Cursor pagination is often preferable for frequently changing feeds because it can provide more stable continuation than page offsets when implemented correctly.
8.4 Pagination Comparison
| Strategy |
Advantages |
Considerations |
| Page-based |
Simple to understand and implement. |
Can be affected by changes in records between requests. |
| Offset-based |
Easy to jump to a particular position. |
Large offsets may become expensive; concurrent changes can shift records. |
| Cursor-based |
Suitable for continuously changing feeds and large datasets. |
Requires backend cursor support and careful cursor management. |
9. Managing Loading States
Loading states communicate what the application is doing and prevent confusing user experiences. A robust screen should distinguish between initial loading, refreshing, loading additional data, and request failures.
9.1 Initial Loading
Initial loading occurs when the screen first requests data. A full-screen spinner or skeleton layout can communicate that the list is being prepared.
{initialLoading ? (
<ActivityIndicator size="large" />
) : (
<FlatList data={items} renderItem={renderItem} />
)}
9.2 Refreshing State
Use the refreshing state to control the pull-to-refresh indicator.
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
The refreshing property is controlled by the application. It should be set to true when refresh begins and reset to false when the request finishes.
9.3 Loading More Data
A footer spinner is usually less disruptive than a full-screen loading indicator when the user is already viewing records.
const ListFooter = () => (
loadingMore
? <ActivityIndicator size="small" />
: null
);
9.4 Avoiding Conflicting States
Applications should prevent a refresh operation and a pagination operation from updating the same list concurrently unless the request logic explicitly supports it. Otherwise, an older response could overwrite newer data or append records to a refreshed list.
10. Handling Errors and Network Problems
Network failures may occur because of poor connectivity, server errors, authentication problems, or request timeouts. Error handling should preserve existing data whenever possible.
10.1 Handle API Errors
async function getData() {
try {
const response = await fetch(
'https://example.com/api/items'
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
10.2 Recommended Error Behavior
- Show a full-screen error and retry action when the initial request fails.
- Preserve previously loaded records if loading the next page fails.
- Keep the current records visible if refreshing fails.
- Display a clear error message without exposing sensitive technical details.
- Provide a retry mechanism for failed pagination requests.
- Ensure loading indicators stop even when a request throws an error.
- Use request cancellation or stale-response protection when screens unmount or queries change.
11. Preventing Duplicate API Requests
The onEndReached callback may fire more than once under certain scrolling and layout conditions. Fast scrolling, short lists, state updates, and network delays can contribute to duplicate requests.
11.1 Common Prevention Techniques
- Use a request lock or in-flight request reference.
- Track the current page or cursor accurately.
- Maintain a reliable
hasMore state.
- Disable pagination while refreshing or performing initial loading.
- Deduplicate records using stable identifiers.
- Use a backend response that explicitly indicates whether another page exists.
- Consider a query library that manages caching and request deduplication.
11.2 Why a Ref Can Help
const loadingRef = useRef(false);
async function loadMore() {
if (loadingRef.current) return;
loadingRef.current = true;
try {
await fetchNextPage();
} finally {
loadingRef.current = false;
}
}
A ref updates immediately without waiting for a React render, making it useful as a lightweight request lock. It should be combined with correct pagination logic and appropriate error handling.
12. Designing an Effective User Experience
12.1 Loading Indicators
Use a refresh spinner for pull-to-refresh and a small footer spinner for pagination. Avoid displaying multiple competing indicators for the same operation.
12.2 Empty States
If the API returns no records, display a meaningful message such as “No posts available yet.” For search screens, explain that no results match the current query.
12.3 End-of-List Messages
When the backend confirms there are no more records, the application may show a small message such as “You have reached the end.” Avoid repeatedly requesting pages after the end has been reached.
12.4 Retry Controls
When a request fails, provide a retry button or an equivalent accessible action. Do not force users to scroll repeatedly just to retry a failed page request.
12.5 Accessibility
- Use readable text sizes and adequate color contrast.
- Provide meaningful labels for retry controls.
- Ensure loading and error states are communicated clearly.
- Do not rely exclusively on color to indicate success or failure.
- Check that custom gestures do not interfere with normal scrolling.
13. Improving FlatList Performance
Efficient rendering becomes important when a list contains hundreds or thousands of records.
13.1 Use Stable Keys
<FlatList
data={items}
keyExtractor={item => String(item.id)}
renderItem={renderItem}
/>
Stable keys help React identify individual records during updates. Avoid using array indexes as keys when records may be inserted, removed, or reordered.
13.2 Memoize List Items
React's memo can reduce unnecessary rerenders when a list item receives unchanged props.
import React, { memo } from 'react';
import { View, Text } from 'react-native';
const ProductItem = memo(function ProductItem({ item }) {
return (
<View>
<Text>{item.name}</Text>
</View>
);
});
Memoization should be applied when profiling shows a benefit. It does not eliminate all rerenders, especially when props or callbacks change.
13.3 Tune Rendering Properties
| Property |
Purpose |
initialNumToRender |
Sets the initial number of items rendered. |
maxToRenderPerBatch |
Controls the number of additional items rendered per batch. |
windowSize |
Controls the size of the render window around the visible region. |
removeClippedSubviews |
Can improve performance by detaching off-screen views on supported configurations, but should be tested for visual issues. |
getItemLayout |
Allows fixed item dimensions and offsets to be calculated in advance. |
13.4 Use getItemLayout for Fixed-Height Items
const ITEM_HEIGHT = 72;
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={item => String(item.id)}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
Only use this configuration when the actual item dimensions match the assumed height. Variable-height items require a different approach.
14. Integrating Pull-to-Refresh with Search and Filters
Some applications allow users to filter or search list records. Refreshing and pagination must preserve the correct query parameters so the results remain consistent.
14.1 Example Filtered Request
async function fetchProducts(page, category) {
const params = new URLSearchParams({
page: String(page),
category,
});
const response = await fetch(
`https://example.com/api/products?${params.toString()}`
);
if (!response.ok) {
throw new Error('Unable to fetch products');
}
return response.json();
}
14.2 Important Considerations
- Reset the page or cursor when the search query changes.
- Clear or replace old results when a new filter is applied.
- Ignore responses associated with outdated search queries.
- Use the same filters for refresh and pagination.
- Prevent a response from a previous query from being appended to the current list.
15. Using AbortController for Request Cancellation
When users leave a screen or change a query, an outstanding request may no longer be useful. Where supported by the React Native version and networking implementation, AbortController can cancel requests that are no longer needed.
async function fetchItems(signal) {
const response = await fetch(
'https://example.com/api/items',
{ signal }
);
if (!response.ok) {
throw new Error('Unable to load items');
}
return response.json();
}
const controller = new AbortController();
fetchItems(controller.signal).catch(error => {
if (error.name !== 'AbortError') {
console.error(error);
}
});
// Call when the request is no longer needed:
controller.abort();
In a React component, create and manage controllers inside an appropriate effect or request lifecycle. Avoid treating an intentional cancellation as an ordinary user-facing network failure.
16. Common Problems and Troubleshooting
| Problem |
Possible Cause |
Solution |
| Refresh indicator does not appear. |
The refreshing property is not controlled correctly. |
Set it to true when refreshing starts and reset it in finally. |
| Refresh callback does not execute. |
onRefresh is missing or the scrollable content is configured incorrectly. |
Check the RefreshControl integration and test the gesture on a device. |
| Pagination fires repeatedly. |
onEndReached can trigger more than once. |
Use request locking and guard against duplicate page requests. |
| Records appear more than once. |
The backend or client returns overlapping pages. |
Deduplicate records using stable IDs and inspect pagination parameters. |
| Some records disappear after refresh. |
The refreshed dataset intentionally replaces old records or a stale request overwrites state. |
Check refresh behavior and coordinate concurrent requests. |
| Loading spinner never stops. |
A code path does not reset the loading state. |
Use finally to reset loading flags. |
| New records never load. |
The page number is not incremented or hasMore is false. |
Inspect the API response and pagination state. |
| List renders blank initially. |
Initial loading, empty-state, or rendering logic is incorrect. |
Check the API response, list data, and empty-state component. |
| Old search results appear. |
A request from a previous query completes late. |
Cancel old requests or ignore stale responses. |
| List scrolling is slow. |
Complex rows, unnecessary rerenders, or excessive image loading. |
Profile rendering, optimize item components, and tune FlatList properties. |
17. Best Practices for Production Applications
- Use
FlatList for long datasets rather than rendering every item inside a basic ScrollView.
- Keep refresh, initial loading, and pagination states logically separate.
- Protect against duplicate requests and duplicate records.
- Use server-supported pagination and respect the response's next-page or continuation information.
- Do not assume that a short page always means the end unless the API contract guarantees it.
- Preserve existing records when a subsequent page fails.
- Use stable unique keys for all list items.
- Cancel or ignore obsolete requests when appropriate.
- Provide empty, loading, error, retry, and end-of-list states.
- Use caching when repeated requests would otherwise download unchanged data.
- Respect authentication, API rate limits, and backend response requirements.
- Test slow networks, offline conditions, rapid scrolling, and repeated refresh gestures.
- Verify behavior on both Android and iOS because native scrolling and refresh interactions can differ.
- Measure performance on physical devices before applying advanced optimizations.
18. Testing Pull-to-Refresh and Infinite Scrolling
Testing should verify both the expected user experience and the correctness of pagination state.
18.1 Functional Test Cases
| Test Case |
Expected Result |
| Open the screen for the first time. |
The first page loads and appears in the list. |
| Pull down to refresh. |
The refresh indicator appears and the latest page is loaded. |
| Scroll near the bottom. |
The next page loads and new items are appended. |
| Reach the final page. |
The application stops requesting additional pages. |
| Refresh while pagination is active. |
Conflicting requests are prevented or coordinated correctly. |
| Simulate a network failure. |
An appropriate error is shown and loading indicators stop. |
| Retry a failed page. |
The application requests the correct page again. |
| Return an empty response. |
A suitable empty state is displayed. |
| Scroll rapidly. |
Duplicate requests and duplicate items are avoided. |
| Change the search filter. |
Old results are cleared or replaced and pagination restarts. |
18.2 Testing Tools
- React Native DevTools: Inspect component behavior and debug JavaScript.
- Android Studio: Test Android builds and investigate device logs.
- Xcode: Run and debug iOS applications on supported macOS environments.
- Jest: Test state transitions, pagination helpers, and API handling.
- React Native Testing Library: Test rendered interfaces and user interactions.
19. Mini Project: News Feed Application
Create a news feed screen that combines pull-to-refresh and infinite scrolling.
19.1 Project Requirements
- Fetch news articles from a paginated API.
- Display each article's title, summary, publication date, and thumbnail.
- Support pull-to-refresh to retrieve the latest articles.
- Load additional articles as the user scrolls downward.
- Display a loading indicator while requesting the next page.
- Display a retry action if the request fails.
- Prevent duplicate articles using their unique IDs.
- Show an empty state if no articles are available.
- Stop pagination when the backend indicates there are no more articles.
19.2 Suggested Component Structure
NewsFeedApp
└── NewsFeedScreen
├── NewsHeader
├── FlatList
│ ├── NewsArticleCard
│ ├── NewsArticleCard
│ └── NewsArticleCard
├── RefreshControl
├── LoadingFooter
├── EmptyState
└── ErrorState
19.3 Suggested Development Steps
- Create the React Native project and configure the required environment.
- Build a reusable news article card component.
- Connect the screen to a paginated API.
- Implement the initial loading state.
- Add pull-to-refresh using
RefreshControl.
- Implement pagination with
onEndReached.
- Add request locking and duplicate-record protection.
- Implement error, empty, and end-of-list states.
- Test with slow network responses and failed requests.
- Optimize list rendering and verify Android and iOS behavior.
20. Practice Exercises
- Create a basic list of users with pull-to-refresh.
- Fetch a collection of products from a REST API.
- Add page-based infinite scrolling to the product list.
- Display a footer spinner while loading additional records.
- Prevent duplicate requests when the user scrolls quickly.
- Add a retry button for failed pagination requests.
- Implement cursor-based pagination using a mock API.
- Combine infinite scrolling with a category filter.
- Preserve existing list data when a refresh request fails.
- Create a news feed with loading, empty, error, and end-of-list states.
21. Interview Questions and Answers
Q1. What is pull-to-refresh in React Native?
Answer: Pull-to-refresh is a gesture that lets users request updated content by pulling a scrollable list downward. It is commonly implemented with RefreshControl.
Q2. What is infinite scrolling?
Answer: Infinite scrolling loads additional records as the user approaches the end of a list. React Native's FlatList supports this through the onEndReached callback.
Q3. What is the difference between refreshing and loadingMore?
Answer: refreshing tracks a refresh operation, while loadingMore tracks a request for additional pages. They usually control different loading indicators.
Q4. Why is FlatList preferred for long lists?
Answer: FlatList uses virtualization to limit the number of list items rendered around the visible region, helping reduce unnecessary rendering and memory usage.
Q5. What does onEndReachedThreshold do?
Answer: It specifies how close the end of the list must be, expressed in visible-list lengths, before the end-reached callback can trigger. A value of 0.5 represents approximately half a viewport from the end.
Q6. Why can onEndReached execute multiple times?
Answer: Scrolling and layout changes can trigger the callback repeatedly. Guarding requests with a lock, checking the current pagination state, and deduplicating records helps prevent unwanted requests and duplicate content.
Q7. Why should loading flags be reset in finally?
Answer: A finally block executes after a successful or failed request, ensuring that loading indicators do not remain active because an exception occurred.
Q8. How should additional records be added to the list?
Answer: Use a functional state update, such as setItems(previous => [...previous, ...newItems]), and deduplicate records when the API can return overlapping results.
Q9. What is cursor-based pagination?
Answer: Cursor-based pagination uses a token or record position supplied by the backend to request the next portion of a dataset. It is useful for feeds that change frequently.
Q10. How can an application detect the end of a list?
Answer: It can use a backend-provided next-page cursor, a hasMore field, a total count, or a documented page-size convention to determine whether more records are available.
Q11. What happens if a pagination request fails?
Answer: The application should stop the loading indicator, preserve the existing list, display an appropriate error, and allow the user to retry the failed request.
Q12. How can FlatList performance be improved?
Answer: Use stable keys, keep row components efficient, memoize components where beneficial, optimize images, and tune rendering properties based on profiling.
22. Quick Revision Checklist
FlatList displays dynamic scrollable datasets efficiently.
RefreshControl provides pull-to-refresh functionality.
refreshing controls the refresh indicator.
onRefresh starts the refresh operation.
onEndReached triggers pagination near the list's end.
onEndReachedThreshold controls the distance at which pagination may trigger.
ListFooterComponent can display a pagination spinner or end message.
ListEmptyComponent can display an empty state.
- Use a stable
keyExtractor for each record.
- Replace the dataset during refresh and append records during pagination when appropriate.
- Use request locks, correct page or cursor tracking, and duplicate-record protection.
- Reset loading states reliably after every request.
- Handle network errors, retries, empty responses, and the end of the dataset.
- Test fast scrolling, slow networks, and Android and iOS behavior.
23. Conclusion
Pull-to-refresh and infinite scrolling are essential interaction patterns for modern React Native applications. Pull-to-refresh lets users retrieve updated information, while infinite scrolling makes large datasets easier to explore without traditional pagination controls.
By combining FlatList, RefreshControl, onRefresh, and onEndReached with effective API integration and state management, developers can create responsive and maintainable list interfaces. Reliable request handling, pagination tracking, duplicate prevention, accessible feedback, and performance testing are particularly important for production applications.
To learn more about React Native development, explore the React Native Training course at JustAcademy or register for a course demo.